/**
 * @description WebSocket
 */

export interface SocketOptions {
  onOpen?: (event: WebSocketEvent) => LooseObject
  onClose?: (event: WebSocketEvent) => LooseObject
  onError?: (event: WebSocketEvent) => LooseObject
  onMessage?: (event: WebSocketEvent) => LooseObject
  url?: string
  protocol?: string | string[]
  binaryType?: BinaryType
  debug?: boolean
}

export interface WebSocketEvent {
  data?: LooseObject
  event?: Event
  message?: string
}

export default class Socket {
  private debug = false
  private url: string
  private options: SocketOptions
  private tag = '[::socket::]'
  private instance: WebSocket

  constructor(url: string, options: SocketOptions = {}) {
    this.options = options
    this.url = url
    this.instance = new WebSocket(this.url, this.options.protocol)
    this.instance.binaryType = this.options.binaryType || 'arraybuffer'
    this.debug = options.debug
    this.instance.addEventListener('open', this.onOpen.bind(this))
    this.instance.addEventListener('close', this.onClose.bind(this))
    this.instance.addEventListener('error', this.onError.bind(this))
    this.instance.addEventListener('message', this.onMessage.bind(this))
  }

  private log(...messages) {
    if (!this.debug) return
    console.log.call(window, this.tag, ...messages)
  }

  private error(...messages) {
    console.error.call(window, this.tag, ...messages)
  }

  isConnecting(): boolean {
    return this.instance.readyState === WebSocket.CONNECTING
  }

  isOpen(): boolean {
    return this.instance.readyState === WebSocket.OPEN
  }

  isClosing(): boolean {
    return this.instance.readyState === WebSocket.CLOSING
  }

  isClosed(): boolean {
    return this.instance.readyState === WebSocket.CLOSED
  }

  parseData(event: LooseObject | Event): WebSocketEvent {
    let data = event?.data
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data)
      } catch {
        // ignore...
      }
    }
    return { event, data }
  }

  send(data: LooseObject) {
    if (this.isOpen()) {
      this.log('send data', data)
      this.instance?.send(data)
    }
  }

  private onOpen(event: Event) {
    const webSocketEvent = this.parseData(event)
    this.log('[onOpen]', webSocketEvent)
    this.options.onOpen?.(webSocketEvent)
  }

  private onClose(event: Event) {
    const webSocketEvent = this.parseData(event)
    this.log('[onClose]', webSocketEvent)
    this.options.onClose?.(webSocketEvent)
  }

  private onError(event: Event) {
    const webSocketEvent = this.parseData(event)
    this.error('[onError]', webSocketEvent)
    this.options.onError?.(webSocketEvent)
  }

  private onMessage(event: Event) {
    const webSocketEvent = this.parseData(event)
    this.log('[onMessage]', webSocketEvent)
    this.options.onMessage?.(webSocketEvent)
  }

  destroy() {
    this.instance.removeEventListener('open', this.onOpen)
    this.instance.removeEventListener('close', this.onClose)
    this.instance.removeEventListener('error', this.onError)
    this.instance.removeEventListener('message', this.onMessage)
    if (!this.isClosing() && !this.isClosed()) {
      this.instance.close()
    }
  }
}
